﻿#region Using References

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.IO.Compression;
using System.Text;
using Beaker.OpenCube.IO;

#endregion

namespace Beaker.OpenCube.Nbt
{
	/// <summary>
	/// Reader for reading NBT files from a stream.
	/// </summary>
	public sealed class NbtReader
	{
		private readonly BigEndianBinaryReader _reader;

		/// <summary>
		/// Create a new NBT Reader.
		/// </summary>
		/// <param name="stream">The stream to read the NBT data from.</param>
		public NbtReader( Stream stream )
		{
			Contract.Requires<ArgumentNullException>( stream != null );
			Contract.Ensures( _reader != null );

			// we can only test for gzip compression when the stream is seekable
			if( stream.CanSeek )
			{
				// rememer position of stream
				long pos = stream.Position;

				// get 2 bytes so we can check if it starts with the magic gzip header
				var buffer = new byte[2];
				int read = stream.Read( buffer, 0, 2 );
				// ignore if we read less as 2 bytes, 
				// exception will probably be raised later while reading uncompressed stream
				// but not enough data for now to determine the correct error

				// move position back
				stream.Position = pos;

				// check for the magic header, if found, use a gzip decompression stream
				if( buffer[0] == 0x1f && buffer[1] == 0x8b )
					stream = new GZipStream( stream, CompressionMode.Decompress );
			}

			// create a reader for big endian data
			_reader = new BigEndianBinaryReader( stream );
		}

		[ContractInvariantMethod]
		private void ContractInvariant()
		{
			Contract.Invariant( _reader != null );
		}

		/// <summary>
		/// Reads a tag from the nbt stream.
		/// </summary>
		/// <returns>The read NBT tag.</returns>
		/// <exception cref="NbtFileFormatException">Unknown TAG value</exception>
		private NbtTag ReadTag()
		{
			Contract.Ensures( Enum.IsDefined( typeof( NbtTag ), Contract.Result<NbtTag>() ) );

			// get tag byte
			var tag = (NbtTag)_reader.ReadByte();

			// verifiy if it is in range
			if( !Enum.IsDefined( typeof( NbtTag ), tag ) )
				throw new NbtFileFormatException( "Unknown TAG value" );

			return tag;
		}

		private static readonly UTF8Encoding Utf8 = new UTF8Encoding( false, true ); // utf8 encoder that THROWS on invalid chars

		/// <summary>
		/// Reads a string from the NBT stream.
		/// </summary>
		/// <returns></returns>
		private string ReadString()
		{
			Contract.Ensures( Contract.Result<string>() != null );

			ushort length = _reader.ReadUInt16();
			byte[] bytes = _reader.ReadBytes( length );

			try
			{
				return Utf8.GetString( bytes );
			}
			catch( DecoderFallbackException x )
			{
				throw new NbtFileFormatException( "Not a valid UTF-8 string", x );
			}
		}

		/// <summary>
		/// Reads A node from the stream.
		/// </summary>
		/// <param name="name">The name as read from the stream.</param>
		/// <param name="value">The value as read from the stream.</param>
		/// <returns>Returns <c>true</c> when a value was available, or <c>false</c> when no more nodes available (TAG_End)</returns>
		private bool ReadNode( out string name, out object value )
		{
			// when bool false, out parameters are null
			Contract.Ensures( Contract.Result<bool>() || Contract.ValueAtReturn( out name ) == null );
			Contract.Ensures( Contract.Result<bool>() || Contract.ValueAtReturn( out value ) == null );
			// when bool true, out paramater name is not null, (value is unknown, because it depends on value read form stream)
			Contract.Ensures( !Contract.Result<bool>() || Contract.ValueAtReturn( out name ) != null );

			NbtTag tag = ReadTag();

			// if tag is of type end, we are done now
			if( tag == NbtTag.End )
			{
				name = null;
				value = null;
				return false;
			}

			// otherwise a name (key) MUST follow
			name = ReadString();

			// and then the payload (value)
			value = ReadData( tag );

			return true;
		}

		/// <summary>
		/// Reads the data for the compound node.
		/// </summary>
		/// <returns>A dictionary of KeyValue pairs holding the data from the NBT stream.</returns>
		private NbtDictionary ReadCompound()
		{
			Contract.Ensures( Contract.Result<NbtDictionary>() != null );

			var dict = new NbtDictionary();

			// read until no more data in compound
			for( ; ; )
			{
				string name;
				object value;

				// read a single node
				bool hasValue = ReadNode( out name, out value );
				if( !hasValue )
					break;

				// add node to dictionary
				dict.Add( name, value );
			}

			return dict;
		}


		/// <summary>
		/// Reads the data for the list node
		/// </summary>
		/// <returns>The list node read from the stream.</returns>
		private IEnumerable ReadList()
		{
			Contract.Ensures( Contract.Result<IEnumerable>() != null );

			// read tag of items in list
			NbtTag itemsType = ReadTag();
			// read number of items in list
			int count = _reader.ReadInt32();

			// determine the system type the tag should map to
			Type systemItemsType = itemsType.GetSystemType();

			IList values;
			if( !systemItemsType.IsGenericTypeDefinition )
				// actual type, so create list of actual type
				values = (IList)Activator.CreateInstance( typeof( List<> ).MakeGenericType( systemItemsType ) );
			else
				values = new List<object>();

			// read the given number of values from the stream
			for( int i = 0; i < count; i++ )
			{
				// read value of given type from stream
				object value = ReadData( itemsType );
				// add value to list
				values.Add( value );
			}

			// return list of all the read values
			return values;
		}

		/// <summary>
		/// Reads data of the give type.
		/// </summary>
		/// <param name="type">The type of the data to read.</param>
		/// <returns>An object containing the value of the requested type.</returns>
		private object ReadData( NbtTag type )
		{
			Contract.Requires<ArgumentOutOfRangeException>( Enum.IsDefined( typeof( NbtTag ), type ) );
			Contract.Ensures( Contract.Result<object>() != null );

			switch( type )
			{
				case NbtTag.Byte:
					return _reader.ReadSByte(); // minecraft uses Signed bytes (weird... is this normal for Java apps?)

				case NbtTag.Short:
					return _reader.ReadInt16();

				case NbtTag.Int:
					return _reader.ReadInt32();

				case NbtTag.Long:
					return _reader.ReadInt64();

				case NbtTag.Float:
					return _reader.ReadSingle();

				case NbtTag.Double:
					return _reader.ReadDouble();

				case NbtTag.ByteArray:
					int length = _reader.ReadInt32();
					return _reader.ReadSBytes( length );

				case NbtTag.String:
					return ReadString();

				case NbtTag.List:
					return ReadList();

				case NbtTag.Compound:
					return ReadCompound();

				case NbtTag.IntArray:
					int count = _reader.ReadInt32();
					var intList = new List<int>();
					for( int i = 0; i < count; i++ )
						intList.Add( _reader.ReadInt32() );
					return intList.ToArray();
			}

			throw new NotImplementedException( "Encountered an tag that cannot be handled" );
		}

		/// <summary>
		/// Deserializes the specified stream into a <see cref="CompoundNode"/> object.
		/// </summary>
		/// <param name="stream">The stream to deserialize the data from.</param>
		/// <returns>The deserialized NBT object.</returns>
		public static NbtDictionary Deserialize( Stream stream )
		{
			Contract.Requires<ArgumentNullException>( stream != null );
			Contract.Ensures( Contract.Result<NbtDictionary>() != null );

			var reader = new NbtReader( stream );

			NbtTag tag = reader.ReadTag();
			if( tag != NbtTag.Compound )
				throw new NbtFileFormatException( "First tag MUST be a compound tag" );

			string ignoredName = reader.ReadString();
			//if( ignoredName.Length != 0 )
			//	throw new NbtFileFormatException( "First compound MUST have empty name" );

			return reader.ReadCompound();
		}
	}
}